import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

public class MainRequestThread extends Thread {

	private static DatagramSocket requestSocket;
	private static String address;
	private static int port;
	private static Integer nextSentSeqNum;
	
	// a dictionary that maps sequence number to a request thread
	private static Map<Byte, RequestThread> rtl = new HashMap<Byte, RequestThread>();
		
	public MainRequestThread(DatagramSocket requestSocket, String address, int port) {
		super();
		MainRequestThread.requestSocket = requestSocket;
		MainRequestThread.address = address;
		MainRequestThread.port = port;
		MainRequestThread.nextSentSeqNum = 0;
	}
	
	// the value of the current sequence counter (used when generating a new request-response packet)
	private static Byte seqNoCounter = 0;
	
	// returns a new sequence number (used when generating a new request-response packet)
	public static byte getNextSeqNo() {
		synchronized (seqNoCounter) {
			byte ret = seqNoCounter;
			seqNoCounter++;
			return ret;
		}
	}
	
	protected static void decrementSeqNo() {
		synchronized (seqNoCounter) {
			seqNoCounter--;
		}
	}
	
	@Override
	public void run() {
		
		Thread wrt = new WaitResponseThread(requestSocket, MainRequestThread.rtl);		
		wrt.start();
		
		Scanner stdin = new Scanner(System.in);
		// loop-read from stdin				
		while(true) {
			System.out.println("[Prompt] Enter 'h' for help. ");
			String commandStr = stdin.nextLine().trim();
			
			if (commandStr.equals("q!")) {
				// notify all the outstanding request threads to die
				killResponseThread();
				for (Byte seqNo : rtl.keySet()) {
					rtl.get(seqNo).close();
				}
				
				// clean up the rtl map
				rtl.clear();
				
				// (Note that WaitResponseThread wrt will die when requestSocket is closed in MainClass)
				break;
			} else if (commandStr.equals("h")) {
				Util.printUsage();
				continue;
			} else if (commandStr.equals("q")) {
				killResponseThread();
				while (!rtl.isEmpty()) {
					System.out.println("[SYSTEM] Waiting for the remaining " + rtl.size() + " requests to finish... ");
					try {
						Thread.sleep(MainClass.TIME_INTERVAL);
					} catch (InterruptedException e) {
						continue;
					}
				}
				break;
			}
			
			// Get the address
			String ipPort = "";
			try {
				ipPort = Util.getOutAddressPort();
			} catch (Exception e) {
				System.out.println("[ERROR] Get outer ip address error. Please try again. ");
				continue;
			}
				
			final byte newSeqNo = getNextSeqNo();
			ICommand commandObj = null;
			try {
				commandObj = ICommand.createInstanceFromCommand(commandStr, newSeqNo, InetAddress.getByName(ipPort.split(" ")[0]).getAddress());
			} catch (Exception e) {
				System.out.println("[ERROR] " + e.getMessage());
				decrementSeqNo();
				continue;
			}
			
			registerNewThread(commandObj, newSeqNo);
			
		}
		stdin.close();
	}
	
	protected static void registerNewThread(ICommand commandObj, final byte newSeqNo) {
		commandObj.setSeqNum(newSeqNo);
		final int type = commandObj.getType();
		
		// create new request thread
		RequestThread rt = new RequestThread(commandObj, requestSocket, address, port, new TimeOutBehavior() {
			
			@Override
			public void behave() {
				System.out.println("[SERVER] " + Util.getCommandTypeInEnglish(type) + " (Sequence number " + newSeqNo + ") timed out. ");
				rtl.remove(newSeqNo);
				
			}
		}, -1);
		
		// store the thread-seqNo correspondence
		rtl.put(newSeqNo, rt);
		
		// run new request thread
		rt.start();
	}
	
	private void killResponseThread() {
		if (WaitResponseThread.rrtl != null) {
			for (IResponse response : WaitResponseThread.rrtl.values()) {
				response.close();
				synchronized (response) {
					response.notify();	
				}
			}
		}
	}
	
	protected static void incrementNextSentSeqNum() {
		MainRequestThread.nextSentSeqNum += 1;
	}
	
	protected static int getNextSentSeqNum() {
		return MainRequestThread.nextSentSeqNum;
	}
}
